import http from "http";
import https from "https";
import fs from "fs";
import path from "path";
import RequestOption from "../request-option";
import DownloadThreadInfo from "./download-threadInfo";
import FileDataInfo from "./file-dataInfo";
import { createHash } from 'crypto'
import Response from "../response";
import logc from "@baige/logc"
const Type = require(path.join(__dirname, "Content-Type.json"))

export = class Download {
  public static async download(option: typeof RequestOption.typeof, mes: string = "请求:"): Promise<Response> {
    logc.info(option.logs, mes, option.url);
    return new Promise(async (resolve, reject) => {
      htt(option.url).get(option.url, option, async function (res: any) {
        //请求成功 关闭延迟监控
        switch (res.statusCode) {
          case 404: {
            logc.warn(option.logs, "这是一个404链接");
            let data = new Response();
            data.res = res;
            data.statusCode = 404;
            data.headers = res.headers;
            data.cookie = option.cookie.encapsulation(option.url);
            data.cookieTree = option.cookie.getCookieTree();
            return resolve(data);
          }
          case 301:
          case 302: {
            // 需要重定向，解析出重定向的位置
            option.url = option.generateRedirectAddress(res);
            resolve(await Download.download(option, "重定向:"));
            break;
          }
          case 206:
          case 200: {
            let fileDataInfo = new FileDataInfo();
            let fileData = pars_file(res.headers, option.url);
            // 解析出文件大小以及文件名
            if (option.saveFilePath) {
              let file = path.parse(option.saveFilePath)
              fileDataInfo.dir = file.dir;
              fileDataInfo.fileName = file.base;
              fileDataInfo.path = option.saveFilePath;
              fileDataInfo.size = fileData.size;
            } else {
              if (!fileData.filename) {
                // 取网址最后一段
                fileData.filename = option.url?.replace(/\?.*$/, "").match(/(?<=\/)[^\/]*?$/g)?.[0] as string
              }
              // 获取文件后缀
              let suffx = Type[res.headers["content-type"].split(";")[0]];
              if (fileData.filename && !/\.[^\/]*?$/.test(fileData.filename) && !fileData.filename.endsWith(suffx) && suffx) {
                fileData.filename += suffx
              }
              fileDataInfo.dir = option.saveFileDir || process.cwd();
              fileDataInfo.fileName = option.saveFileName || fileData.filename;
              // 这是临时文件路径
              fileDataInfo.path = fileDataInfo.dir + "/" + fileDataInfo.fileName;
            }

            // 解析路径信息
            let fileInfo = createAvailablename(fileDataInfo.path);
            fileDataInfo.fileName = fileInfo.base;
            fileDataInfo.path = path.join(fileInfo.dir, "未完成" + md5(fileInfo.name) + ".cdownload")
            fileDataInfo.size = fileData.size;

            if (option.original) {
              let data = new Response();
              data.res = res;
              data.statusCode = res.statusCode
              data.headers = option.headers;
              data.cookieTree = option.cookie.getCookieTree();
              data.cookie = option.cookie.encapsulation(option.url);;
              data.data = fileDataInfo;
              // 需要原始对象就返回，不进行下载
              return resolve(data);
            }

            // 创建文件夹
            fs.mkdirSync(fileInfo.dir, { recursive: true });

            // 如果是多线程就执行多线程下载
            if (res.headers["accept-ranges"] === "bytes") {
              // 支持多线程下载
              Download.multithread(option, fileDataInfo, 16).then(_e => {
                if (option.logs)
                  console.log();
                // 下载完成，删除状态数据
                fs.truncateSync(fileDataInfo.path, Number(fileDataInfo.size))
                // 修改文件名
                fs.renameSync(fileDataInfo.path, fileDataInfo.dir + "/" + fileDataInfo.fileName)
                let res = new Response();
                res.data = {
                  result: true,
                  path: fileDataInfo.dir + "/" + fileDataInfo.fileName
                } as object;
                resolve(res);
              }).catch(reject)
            } else {
              // 单线程下载
              Download.singleThread(option, fileDataInfo).then(_e => {
                if (option.logs)
                  console.log();
                fs.renameSync(fileDataInfo.path, fileDataInfo.dir + "/" + fileDataInfo.fileName)
                let res = new Response();
                res.data = {
                  result: true,
                  path: fileDataInfo.dir + "/" + fileDataInfo.fileName
                } as object;
                resolve(res);
              }).catch(reject)
            }
          }
        }
      }).on("error", reject).end();
    })

  }





  public static async multithread(option: typeof RequestOption.typeof, fileInfo: typeof FileDataInfo.typeof, totalThreadNumber: number = 16) {
    let size = 0, bf = 0;
    // 这个负责下载文件的状态
    let downloadThreadInfo: DownloadThreadInfo = new DownloadThreadInfo();
    downloadThreadInfo.totalThreadNumber = totalThreadNumber;
    // 检测这个文件是否被下载过 是否拥有状态文件以及缓存文件
    if (fs.existsSync(fileInfo.path)) {
      // 不需要再创建文件
      logc.fatal(option.logs, "检测到缓存，开始恢复下载");
      try {
        // 导入状态文件
        downloadThreadInfo = await readState(fileInfo);
      } catch (err) {
        logc.error(option.logs, "缓存导入失败，重新下载")
        // 报错了,删除缓存和状态文件
        fs.unlinkSync(fileInfo.path)
        fs.writeFileSync(fileInfo.path, "");
      }
    } else {
      // 这是新文件
      fs.writeFileSync(fileInfo.path, "");
    }

    // 这个是分段数据的起点
    let start_location = 0;
    // 开始创建文件状态信息
    if (downloadThreadInfo.totalThreadNumber != 1) {
      file_piecewise(fileInfo.size, downloadThreadInfo.totalThreadNumber).forEach((e, index) => {
        if (downloadThreadInfo.threads[index]) {
          size += downloadThreadInfo.threads[index].cumulative;
          // 继续下载
          // 起点=起点＋累计
          downloadThreadInfo.threads[index].start = start_location + downloadThreadInfo.threads[index].cumulative;
          // 终点不变，累计不变
        } else {
          // 新下载线程
          downloadThreadInfo.threads[index] = {
            start: start_location,
            end: e,
            cumulative: 0,
            // 这部分数据是否已经被下载完成
            isComplete: false
          }
        }
        start_location = e + 1;
      })
    }

    // 把线程的基本信息写入文件
    writeState(fileInfo, downloadThreadInfo)

    logc.fatal(option.logs, "多线程下载：" + option.url)
    // 监控进程结束信号
    process.on('SIGINT', function () {
      // 在程序结束前把数据写入文本
      writeState(fileInfo, downloadThreadInfo)
      process.exit();
    });

    await Promise.all(downloadThreadInfo.threads.map(async (item, index) => {
      // 为真代表这部分数据已经下载完成，直接结束这个下载进程
      if (item.isComplete === true) return
      await this.downloadCore(option.cope(), { ...fileInfo, code: index }, item, (len: number, code: number) => {
        // 接收到数据信息
        downloadThreadInfo.threads[code].cumulative += len;
        writeState(fileInfo, downloadThreadInfo)
        size += len;
        bf = progress(option, fileInfo, size, bf);
      }).catch(err => { console.log(err); })
    })).catch(err => {
      console.log(err);
    })
    return fileInfo.path;
  }


  /**
 * 单线程下载
 */
  public static async singleThread(option: typeof RequestOption.typeof, fileInfo: typeof FileDataInfo.typeof) {
    let size = 0, bf = 0;
    logc.info(option.logs, "单线程下载：" + option.url);
    // 创建下载文件
    fs.writeFileSync(fileInfo.path, "")
    // 只能进行单线程下载
    await this.downloadCore(option, { ...fileInfo, code: 0 }, { start: 0, end: fileInfo.size, isComplete: false, cumulative: 0 }, (len: number) => {
      size += len;
      // 进度
      bf = progress(option, fileInfo, size, bf);
    })
    return fileInfo.path;
  }


  /**
   * 下载核心
   * @param option 
   * @param fileInfo 
   * @param downloadThreadInfo 
   * @param call 
   * @returns 
   */
  public static downloadCore(option: typeof RequestOption.typeof, fileInfo: typeof FileDataInfo.typeof, threadInfo: { start: number; end: number; isComplete: boolean; cumulative: number }, call = (_num: number, _code: number) => { }) {
    // 零时起点
    let start = threadInfo?.start || 0;
    option.headers = { ...option.headers, "Range": "bytes=" + [threadInfo.start, threadInfo.end].join("-") }
    return new Promise((resolve, reject) => {
      try {
        let req = (option.url.startsWith("https") ? https : http).get(option.url, option, (res) => {
          // 创建写入流
          let stream = fs.createWriteStream(fileInfo.path, { start, flags: "r+" });
          stream.on("error", reject);
          res.pipe(stream).on("close", () => { resolve(true) })
          res.on("data", (num: any) => {
            start += num.length;
            // 为真表示这段时间来数据了
            call(num.length, fileInfo.code)
          })
          res.on("end", () => {
            // 结束监控
            threadInfo.isComplete = true;
            // 执行回调
            call(0, fileInfo.code);
          })
          // 出现错误不用管，定时检查程序自己会处理
          res.on("error", reject)
        })
        // 出现错误不用管，定时检查程序自己会处理
        req.on("error", reject)
      } catch (err) {
        reject(err)
        console.log(err);
      }
    })
  }

}




function htt(url: string) {
  return url.startsWith("https") ? https : http
}

// 写入json
function writeState(fileData: typeof FileDataInfo.typeof, data: typeof DownloadThreadInfo.typeof) {
  let w = fs.createWriteStream(fileData.path, { start: Number(fileData.size), flags: "r+" });
  w.write(JSON.stringify(data))
  w.close();
}

// 读取json文件
function readState(fileData: typeof FileDataInfo.typeof): any {
  let r = fs.createReadStream(fileData.path, { start: Number(fileData.size) });
  let data = "";
  return new Promise((res, callerror) => {
    r.on("data", a => data += a)
    r.on("end", (_: any) => {
      try {
        res(JSON.parse(data))
      } catch (err) {
        callerror(data)
      }
    })
  })
}

function md5(data: string) {
  return createHash("md5").update(String(data)).digest("hex")
}

// 把文件分段 piecewise
function file_piecewise(size: any, num: any) {
  size = Number(size)
  num = Number(num)
  let a = [];
  let b = Number((size / num).toFixed(0));
  for (let i = 1; i <= num; i++)
    a.push(b * i)
  a[a.length - 1] += size - (b * num)
  return a;
}

//下载进度显示
function progress(option: typeof RequestOption.typeof, total: FileDataInfo, now: number, superpro: number): number {
  let mb = (num: number) => Number(Number(num / 1024 / 1024).toFixed(2));
  if (!total.size) {
    // 总量等于0 说明不是一个正常的文件，无法显示进度，只能显示下载量
    if (option.logs)
      process.stdout.write(`\r进度：${mb(now)}MB  ${total.fileName}  `)
    return 0;
  } else {
    let percentage = Number((now / total.size * 100).toFixed(2));
    if (superpro < percentage) {
      if (option.logs)
        process.stdout.write(`\r进度：${percentage}%  ${total.fileName}  `);
      return percentage
    }
  }
  return superpro
}


/**
 * 获取文件信息
 * @param obj 
 * @param urls 
 * @returns 
 */
let pars_file = function (obj: any, urls: string): { url: string, filename: string, size: number, path: string } {
  let name = "";
  if (obj["content-disposition"]?.includes("filename=")) {
    name = obj["content-disposition"]?.substring(obj["content-disposition"].indexOf("filename=") + 9)
  }
  // 转为中文
  return {
    url: urls,
    filename: url(name.replace(/["]/g, "")),
    size: obj["content-length"] || 0,
    path: ""
  }
}


// 转义url
function url(str: any) {
  if (!str || !/%[A-Za-z0-9]{2}/.test(str)) { return str; }
  return Array.from(Buffer.from(str.match(/%[A-Za-z0-9]{2}/g)
    .join("").replace(/%/g, ""), "hex").toString())
    .reduce((par, e) => {
      //e如果是中文需要3对
      if (/[^\x00-\xff]/.test(e)) { return par.replace(/(%[A-Za-z0-9]{2}){3}/, e) }
      return par.replace(/%[A-Za-z0-9]{2}/, e);
    }, str);
}


// 创建可用的文件名
function createAvailablename(filepath: string): path.ParsedPath {
  // 解析出后缀
  let fileData = path.parse(filepath)
  if (!fs.existsSync(filepath)) return fileData;
  let i = 2;
  while (true) {
    if (!fs.existsSync(path.join(fileData.dir, fileData.name + `(${i})` + fileData.ext))) {
      // 这个文件名称可用
      return path.parse(path.join(fileData.dir, fileData.name + `(${i})` + fileData.ext))
    }
    i++;
  }
}